package org.iori.util.io;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.ByteOrder;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.iori.util.text.StringUtil;


/**
 * The util class for I/O process
 * 
 * @author zhang_jianbin
 * @history
 *         <p>
 *         2006-5-18 Created Zhang Jianbin
 * 
 */
public class IOUtil {
	private static Log log = LogFactory.getLog(IOUtil.class);

	/**
	 * Copy file to another one.
	 * 
	 * @param sourceFileName
	 *            the source file name with absolute path
	 * @param destFilename
	 *            the destination file name with absolute path
	 * @throws IOException
	 *             when error occurs
	 */
	public static void copyFile(String sourceFileName, String destFilename)
			throws IOException {

		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(
				sourceFileName));

		BufferedOutputStream bos = new BufferedOutputStream(
				new FileOutputStream(destFilename));

		int ch;
		long count = 0;
		while (-1 != (ch = bis.read())) {
			bos.write(ch);
			count++;
		}
		bis.close();

		bos.flush();
		bos.close();

		if (log.isDebugEnabled()) {
			log.debug("Copy file, from " + sourceFileName + " TO "
					+ destFilename + " (size: "
					+ new File(sourceFileName).length() + " -> " + count
					+ " bytes)");
		}
	}

	/**
	 * Copy file to another one.
	 * 
	 * @param srcFileName
	 *            the source file name with absolute path
	 * @param destFilename
	 *            the destination file name with absolute path
	 * @throws IOException
	 *             when error occurs
	 */
	public static void moveFile(String srcFileName, String destFilename)
			throws IOException {

		copyFile(srcFileName, destFilename);
		new File(srcFileName).delete();
	}

	/**
	 * Return the filename that the extension is lowercase format.
	 * 
	 * @param filename
	 * @return the lowercase filename
	 */
	public static String lowercaseFileExtension(String filename) {
		if (null == filename) {
			return null;
		}

		int pos = filename.lastIndexOf(".");
		if (pos != -1) {
			filename = filename.substring(0, pos) + "."
					+ filename.substring(pos + 1).toLowerCase();
		}

		return filename;
	}

	/**
	 * Return the file extension.
	 * 
	 * @param filename
	 * @return the extension, empty string is returned if no extension
	 */
	public static String getFileExtension(String filename) {
		if (null == filename) {
			return null;
		}

		int pos = filename.lastIndexOf(".");
		if (pos == -1) {
			return "";
		}

		return filename.substring(pos + 1);
	}

	/**
	 * Return the file extension.
	 * 
	 * @param file
	 *            the file object
	 * @return the extension, empty string is returned if no extension
	 */
	public static String getFileExtension(File file) {
		if (null == file) {
			return null;
		}

		return getFileExtension(file.getAbsolutePath());
	}

	/**
	 * Return the base filename(without extension name)
	 * 
	 * @param filename
	 *            the filename or the filename with absoulte path
	 * @return the base filename, if the filename is a directory, returns ""
	 *         string.
	 */
	public static String getBaseFilename(String filename) {
		if (null == filename) {
			return null;
		}

		return getBaseFilename(new File(filename));
	}

	/**
	 * Return the base filename(without extension name)
	 * 
	 * @param file
	 *            the file object
	 * @return the base filename, if the filename is a directory, returns ""
	 *         string.
	 */
	public static String getBaseFilename(File file) {
		if (null == file) {
			return null;
		}

		if (file.isDirectory()) {
			return "";
		}

		String name = file.getName();
		int pos = name.lastIndexOf(".");
		if (pos == -1) {
			return name;
		}

		return name.substring(0, pos);
	}

	/**
	 * Return a filename by concatenating the basename and the extension name
	 * 
	 * @param basename
	 *            the basename
	 * @param extensionFrom
	 *            the extension name from which
	 * @return the filename, if the extension name is empty, the filename is
	 *         only the basename
	 */
	public static String concatFilename(String basename, String extensionFrom) {
		String filename = basename;
		String ext = IOUtil.getFileExtension(extensionFrom);
		if (ext.trim().length() != 0) {
			filename += "." + ext;
		}

		return filename;

	}

	/**
	 * 按照指定的文字编码读取内容。
	 * 
	 * @param is
	 *            输入流
	 * @param encoding
	 *            文字编码集
	 * @return 读入的内容
	 * @throws IOException
	 */
	public static String readString(InputStream is, String encoding)
			throws IOException {
		StringBuffer sb = new StringBuffer();

		byte[] buffer = new byte[1024];
		int n;
		while ((n = is.read(buffer)) != -1) {
			sb.append(new String(buffer, 0, n, encoding));
		}
		is.close();

		return sb.toString();
	}

	/**
	 * 读取字节流。
	 * 
	 * @param is
	 *            输入流
	 * @return 读入的字节流
	 * @throws IOException
	 */
	public static byte[] readBytes(InputStream is) throws IOException {
		ByteArrayOutputStream os = new ByteArrayOutputStream();

		byte[] buffer = new byte[1024];
		int n;
		while ((n = is.read(buffer)) != -1) {
			os.write(buffer, 0, n);
		}
		is.close();

		return os.toByteArray();
	}

	/**
	 * Read characters from the Reader stream.
	 * 
	 * @param reader
	 *            the underlying Reader object.
	 * @return the String contains all the characters readed and it is trimed.
	 * @throws IOException
	 */
	public static String readChars(Reader reader) throws IOException {
		return readChars(reader, true);
	}

	/**
	 * Read characters from the Reader stream.
	 * 
	 * @param bis
	 *            the underlying Reader object.
	 * @param length
	 *            the specified length to read
	 * @return the String contains all the characters readed. The white space
	 *         characters at beginning are skipped.
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public static String readChars(Reader bis, int length) throws IOException {
		StringBuffer sb = new StringBuffer();

		// skip the whitespace at beginning
		int ch = skipWhitespace(bis);
		// If all characters are whitespace
		if (-1 == ch) {
			bis.close();
			return sb.toString();
		}

		// Add the first non-whitespace character
		int count = 1;
		sb.append((char) ch);

		// read the specified length(MAX)
		while ((ch = bis.read()) != -1 && count < length) {
			sb.append((char) ch);
			count++;
		}
		bis.close();

		// // Determine the last character
		// if (ch != -1) {
		// sb.append("...");
		// }

		return sb.toString();
	}

	/**
	 * Read characters from the Reader stream.
	 * 
	 * @param reader
	 *            the underlying Reader object.
	 * @param trimFlag
	 *            indicated whether trim the read content.
	 * @return the StringBuffer contains all the characters readed.
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public static String readChars(Reader reader, boolean trimFlag)
			throws IOException {
		StringBuffer sb = new StringBuffer();

		int count = 0;
		int n;
		if (trimFlag) {
			// skip whitespace at beginnig
			n = skipWhitespace(reader);

			// If all characters are whitespace
			if (-1 == n) {
				reader.close();
				return sb.toString();
			}

			// Add the first non-whitespace character
			count = 1;
			sb.append((char) n);
		}

		// read all the characters
		char[] buffer = new char[1024];
		while ((n = reader.read(buffer)) != -1) {
			sb.append(buffer, 0, n);
			count += n;
		}
		reader.close();
		buffer = null;

		if (trimFlag) {
			// Right trim
			sb.setLength(StringUtil.getRTrimPosition(sb) + 1);
		}

		return sb.toString();
	}

	/**
	 * Write the characters in the StrinBufer to the writer
	 * 
	 * @param out
	 *            the underlying writer
	 * @param sb
	 *            the StringBuffer
	 * @throws IOException
	 */
	public static void write(Writer out, StringBuffer sb) throws IOException {
		BufferedWriter bw = null;
		if (out instanceof BufferedWriter) {
			bw = (BufferedWriter) out;
		} else {
			bw = new BufferedWriter(out);
		}

		// int count = 0;
		for (int i = 0; i < sb.length(); i++) {
			bw.write(sb.charAt(i));

			// flush the writting buffer
			if (i != 0 && i % 512 == 0) {
				bw.flush();
			}
		}

		// last flush the writting buffer
		bw.flush();
	}

	/**
	 * Transfer text data from the Reader to the Writer.
	 * 
	 * @param writer
	 *            the underlying Writer object, text data are wrote to it.
	 * @param prefixString
	 *            the text wrote to the Writer before the data from Reader is
	 *            wrote. This value may be null.
	 * @param reader
	 *            the underlying Reader object. the data are read from it.
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public static void write(Writer writer, String prefixString, Reader reader)
			throws IOException {
		if (null != prefixString) {
			writer.write(prefixString);
		}

		char[] buffer = new char[1024];
		int n;
		while ((n = reader.read(buffer)) != -1) {
			writer.write(buffer, 0, n);
		}
		reader.close();
	}

	/**
	 * Transfer text data from the Reader to the Writer. <br>
	 * It will write a new line characters at the end.
	 * 
	 * @param writer
	 *            the underlying Writer object, text data are wrote to it.
	 * @param prefixString
	 *            the text wrote to the Writer before the data from Reader is
	 *            wrote. This value may be null.
	 * @param reader
	 *            the underlying Reader object. the data are read from it.
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public static void writeln(Writer writer, String prefixString, Reader reader)
			throws IOException {
		write(writer, prefixString, reader);
		writer.write("\r\n");
	}

	/**
	 * Write the string content to the Writer. <br>
	 * It will write a new line characters at the end.
	 * 
	 * @param osw
	 *            the underlying Writer object, text data are wrote to it.
	 * @param prefixString
	 *            the text wrote to the Writer before the string content is
	 *            wrote. This value may be null.
	 * @param value
	 *            the string content. The value will be trimed before writting.
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public static void writeln(Writer osw, String prefixString, String value)
			throws IOException {
		osw.write(prefixString);
		if (value != null && value.trim().length() != 0) {
			osw.write(value);
		}
		osw.write("\r\n");
	}

	/**
	 * Write the string content to the Writer. <br>
	 * It will write a new line characters at the end.
	 * 
	 * @param writer
	 *            the underlying Writer object, text data are wrote to it.
	 * @param prefixString
	 *            the text wrote to the Writer before the string content is
	 *            wrote. This value may be null.
	 * @param value
	 *            the string content. The value will be trimed before writting.
	 * @param escape
	 *            the escape expression. If it is a valid esaceping expression,
	 *            the escaped content will be wrote.
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	public static void writeln(Writer writer, String prefixString,
			String value, String escape) throws IOException {
		String val = StringUtil.escapeValue(value, escape);
		writeln(writer, prefixString, val);
	}

	/**
	 * Skip the whitespace, return the first non-whitespace character. If at the
	 * end of Reader, return -1
	 * 
	 * @param reader
	 * @return the charcters skipped
	 * @throws IOException
	 *             if an I/O error occurs
	 */
	private static int skipWhitespace(Reader reader) throws IOException {
		int n;
		while ((n = reader.read()) != -1 && Character.isWhitespace((char) n)) {
		}
		return n;
	}

	/**
	 * Output the file content.
	 * <p>
	 * Note:
	 * <ul>
	 * <li>The output stream will not be closed after calling this method.
	 * <li>If the file doesn't exist, this method do nothing.
	 * </ul>
	 * 
	 * @param filename
	 *            the filename
	 * @param out
	 *            the OutputStream object
	 * @throws IOException
	 *             when an I/O error occurs
	 */
	public static void outputFile(String filename, OutputStream out)
			throws IOException {
		if (null == filename) {
			return;
		}

		if (log.isDebugEnabled()) {
			log.debug("filename: " + filename);
		}

		BufferedOutputStream bos = null;
		if (out instanceof BufferedOutputStream) {
			bos = (BufferedOutputStream) out;
		} else {
			bos = new BufferedOutputStream(out);
		}

		File file = new File(filename);

		if (!file.exists()) {
			bos.close();
			if (log.isErrorEnabled()) {
				log.error("File " + filename + " does not exist.");
			}
			return;
		}

		BufferedInputStream bin = new BufferedInputStream(new FileInputStream(
				file));
		byte[] buffer = new byte[1024 * 10];
		int n;
		while ((n = bin.read(buffer)) != -1) {
			bos.write(buffer, 0, n);
			bos.flush();
		}
		bos.flush();
		bin.close();
	}

	/**
	 * Convert the source text file to another encoding and replace the chars
	 * according the char mapping.
	 * 
	 * 
	 * @param srcFilename
	 *            the source text file.
	 * @param srcEncoding
	 *            the source file encoding.
	 * @param destFilename
	 *            the destination file to output the converted content.
	 * @param destEncoding
	 *            the destination encoding.
	 * @param charMapping
	 *            the Map data contains the char mapping to replace the source
	 *            char to the destination char. The key property indicates the
	 *            source char code under unicode encoding. The value property
	 *            indicates the destination code under destination encoding.
	 *            Both of codes are hex format. <br>
	 *            The source char not in the char mapping is reserved in the
	 *            destination file.
	 * 
	 * @throws IOException
	 */
	public static void convertEncoding(String srcFilename, String srcEncoding,
			String destFilename, String destEncoding, Map charMapping)
			throws IOException {
		InputStreamReader in = new InputStreamReader(new BufferedInputStream(
				new FileInputStream(srcFilename)), srcEncoding);
		OutputStreamWriter out = new OutputStreamWriter(
				new BufferedOutputStream(new FileOutputStream(destFilename)),
				destEncoding);

		int ch;
		boolean firstChar = true;
		String code;
		while (-1 != (ch = in.read())) {
			// process UTF-8 first special identified char FEFF
			if (firstChar) {
				firstChar = false;
				if ("UTF-8".equalsIgnoreCase(srcEncoding)
						&& !"UTF-8".equalsIgnoreCase(destEncoding)
						&& ch == 0xFEFF) {
					continue;
				}
			}

			code = (String) charMapping.get(Integer.toHexString(ch)
					.toUpperCase());
			if (null == code) {
				out.write(ch);
			} else {
				byte[] bytes = new byte[code.length() / 2];
				for (int i = 0, j = 0; i < code.length(); i += 2, j++) {
					bytes[j] = (byte) Integer.parseInt(
							code.substring(i, i + 2), 16);
				}

				out.write(new String(bytes, destEncoding));
			}
		}

		in.close();

		out.flush();
		out.close();
	}
}